import re
import tokenize
import operator
import string
import itertools
import simpleparse
from simpleparse.common import numbers, strings
from simpleparse.parser import Parser
from functools import partial

# Auxiliary regexes
re_name = re.compile('^[a-zA-Z_]\w*')
re_int = re.compile('^-?[0-9]+')
re_dq_string = re.compile(r'^"[^\n"\\]*(?:\\.[^\n"\\]*)*"')
re_sq_string = re.compile(r"^'[^\n'\\]*(?:\\.[^\n'\\]*)*'")

def mk_getter(k, keys=True):
    if keys:
        return k
    else:
        return lambda x: x[k]

#===============================================================================
#                              Array Operations 
#===============================================================================
class array_op(object):
    re_select = re.compile('^\d+\s*(,\d+\s*)*\s*\Z')
    re_sort = re.compile(r'^\s*#.\s*([a-zA-Z]\w*)')
    re_walk = re.compile('^[.]([a-zA-Z]\w*|[-]?[0-9]+)')
    re_filter = re.compile(r'^\s*(@.(?:[a-zA-Z]\w*))\s*(==|!=|>=|<=|<|>|is)?\s*(%s)?%s?\s*\Z' %
                        (tokenize.Number.replace('[lL]+', '').replace('(', '(?:'),
                         tokenize.String))

    def __new__(cls, op):
        '''
        Initialize array operation with fmt argument.
        
        Arguments
        ---------
        
        op : str
            Formatting argument (without brakets)
            
        Examples
        --------
        
        >>> array_op('1,2,3')
        select(1,2,3)
        
        >>> array_op('?(@.foo <= 1)')
        filter(@.foo <= 1)
        
        >>> array_op('#(@.foo)')
        sort(@.foo)
        '''

        if cls.__name__ is 'array_op':
            aop = op.strip()

            if aop.startswith('?'):
                return filter.from_op(aop)
            elif aop.startswith('#'):
                return sort.from_op(aop)
            elif cls.re_select.match(aop):
                return select.from_op(aop)
            elif ':' in aop:
                return slice.from_op(aop)
            else:
                raise ValueError("invalid array operation, %s" % aop)

        else:
            return object.__new__(cls)

    def __repr__(self):
        return '<%s(%s)>' % (type(self).__name__, self.op)

    __str__ = __repr__

class select(array_op):
    def __init__(self, op):
        print(op)
        super(select, self).__init__(op)
        self.list = map(int, op.split(','))

    def __call__(self, obj):
        return [ obj[i] for i in self.list ]

class filter(array_op):
    filter_operators = {
        '==': operator.eq,
        '!=': operator.ne,
        '>=': operator.ge,
        '<=': operator.le,
        '>': operator.gt,
        '<': operator.lt, }

    def __call__(self, obj):
        try:
            key, op, num, st = re_filter.findall(filter_)[0]
        except IndexError:
            raise KeyError("invalid array filter: ?(%s)" % filter_)
        keys = key.split('.')

        # define the getter function
        key = key.lstrip('@')
        walk = self.walk
        def getitem(x):
            try:
                obj, p = walk(x, key)
                assert not p, 'invalid traling path: %s' % p
                return obj
            except KeyError:
                return None

        # simple filter: check if properties exist
        if not op:
            f = lambda x: not (getitem(x) is None)
            return filter(f, obj)

        # define acceptance criteria
        op = filter_operators[op]
        if num:
            try:
                other = int(num)
            except ValueError:
                other = float(num)
        elif st:
            other = st[1:-1].decode('string_escape')
        else:
            raise KeyError("invalid filter '%s'" % filter)

        # check which elements in the array satisfy the acceptance criteria
        def accept(x):
            item = getitem(x)
            if not other is None:
                return op(item, other)
        return filter(accept, obj)

class sort(array_op):
    def __init__(self, path):
        if not hasattr(self, 'walker'):
            self.op = '@' + path[1:]
            path = iterpath(path, flat=True)
            self.walker = SimplePathWalker(list(path))

    @classmethod
    def from_op(cls, op):
        assert op.startswith('#(')
        assert op.endswith(')')
        op = op[2:-1]
        return sort('$' + op[1:])


    def __call__(self, obj):

        keys = key.split('.')

        # define the getter function
        key = key.lstrip('@')
        walk = self.walk
        def getitem(x):
            try:
                obj, p = walk(x, key)
                assert not p, 'invalid traling path: %s' % p
                return obj
            except KeyError:
                return None

        # simple filter: check if properties exist
        items = [ (x, getitem(x)) for x in obj ]
        items.sort(key=lambda x: x[1])
        nulls = [ x[0] for x in items if x[1] is None ]
        non_nulls = [ x[0] for x in items if not x[1] is None ]
        return non_nulls + nulls

class slice(array_op):
    pass



#===============================================================================
#                             Dispacthers 
#===============================================================================
class SimplePathWalker(object):
    """
    SimplePathWalker
    """

    __slots__ = [ '_path' ]
    def __init__(self, path):
        assert isinstance(path, list), 'bad type, %s' % type(path)
        self._path = path

    @classmethod
    def obj_from_string(cls, path):
        return cls(list(cls.iter_path_string(string)))

    @property
    def path(self):
        return '$.' + '.'.join(map(str, self._path))

    def add_node(self, node):
        self._path.append(node)

    def get_value(self, obj):
        try:
            for node in self._path:
                obj = obj[node]
            return obj
        except KeyError as ex:
            raise type(ex)(self.path)

    def __repr__(self):
        return 'SimplePathWalker(%s)' % self.path

    __str__ = __repr__

class PathDispatcher(object):
    def __init__(self, path):
        self._path = path
        self._parsed = self.PARSER.parse(path)
        self._parsed_list = []
        self._parsed_generator = None

    GRAMMAR = r"""# JSON Path grammar
    root          := '$', ('.', [ ]*, (name_node / str_node / number_node / query_node), ('[', [ ]*, (array_cvs / array_filter / array_sort / array_slice), [ ]*, ']')*)+, [ ]*
    
    # node body items
    name_node     := [a-zA-Z_], [a-zA-Z_0-9]*
    str_node      := string
    number_node   := int
    query_node    := '*'
    
    # array operations
    array_cvs     := int, ([ ]*, ',', [ ]*, int)+
    array_filter  := '?(' 
    array_sort    := '#('
    array_slice   := ':'
    """
    PARSER = Parser(GRAMMAR)

    def get_value(self, obj):
        path = self._path
        for func in self._parsed:
            obj = func(obj)
        return obj

    @staticmethod
    def get_from_key(key, obj):
        return obj[key]

    @staticmethod
    def name_node(name):
        return name

    @staticmethod
    def str_node(st):
        return st[1:-1]

    @staticmethod
    def number_node(num):
        return int(num)

    @staticmethod
    def array_cvs(path, *args):
        return [ int(path[start:end]) for (_, start, end, __) in args ]

    @staticmethod
    def array_cvs_op(idxs, obj):
        return [ obj[i] for i in idxs ]

    @property
    def _parsed(self):
        # Consume list
        lst = self._parsed_list
        for x in lst:
            yield x

        # Consume generator
        gen = self._parsed_generator
        gen = (gen if gen is not None else self._parse())
        self._parsed_generator = gen
        if gen:
            for x in gen:
                lst.append(x)
                yield x
        self._parse_generator = False

    def _parse(self):
        fmt = self._parsed_list
        path = self._path
        identity_transformation = lambda x: x
        ret_value = None
        path_walker = []

        for item in self._parsed[1]:
            type, start, end, args = item

            if type.endswith('node'):
                key = path[start:end]
                key = getattr(self, type)(key)
                path_walker.append(key)
                continue

            # Resets path walker
            yield SimplePathWalker(path_walker).get_value
            path_walker = []

            # Format array operator
            arg = getattr(self, type)(path, *args)
            func = partial(getattr(self, type + '_op'), arg)
            yield func

        if path_walker:
            yield SimplePathWalker(path_walker).get_value

    #===========================================================================
    # Alternative implementation
    #===========================================================================
    class LastIterator(object):
        def __init__(self, iterable):
            self.iterable = iter(iterable)
            self.last = None

        def next(self):
            try:
                next = self.iterable.next()
                self.last = next
                return next
            except StopIteration as ex:
                self.last = None
                raise ex

        def __iter__(self):
            return self


    STATE_MAP = { '.': 0, '[': 4, '*': 5, '"': 2, "'": 2 }
    STATE_MAP.update(dict((str(x), 3) for x in range(10)))
    STATE_MAP.update(dict((x, 1) for x in string.ascii_letters + '_'))

    def get_value_char(self, obj):
        NONE, NAME, STR_NAME, NUMBER, ARRAY_OP, QUERY = range(6)
        STATE_MAP = self.STATE_MAP
        state = NONE

        char_it = self.LastIterator(self._path[1:])
        pendding = None
        while True:
            if pendding is None:
                try:
                    c = char_it.next()
                except StopIteration:
                    break
            else:
                c = pendding
                pendding = None

            # Initialize state
            if state is NONE:
                if c == ' ':
                    continue
                else:
                    Q = [c]
                    state = STATE_MAP[c]

            if state is NAME:
                key = ''.join(itertools.takewhile(str.isalnum, char_it))
                obj = obj[c + key]
                pendding, state = char_it.last, NONE

            elif state is STR_NAME:
                raise NotImplemented

            elif state is NUMBER:
                key = ''.join(itertools.takewhile(str.isalnum, char_it))
                obj = obj[int(c + key)]
                pendding, state = char_it.last, NONE

            elif state is ARRAY_OP:
                array_op = ''.join(itertools.takewhile(lambda x: x != ']', char_it))
                obj = ArrayOperation(array_op).get_value(obj)
                state = NONE

        return ob

class Foo:
    def recursive_descent(self, obj, path):
        visited = self.visited
        qset = self.queryset
        if id(obj) in visited:
            return
        else:
            visited.append(id(obj))

        if isinstance(obj, (basestring, int, float, bool, type(None))):
            return

        if isinstance(obj, collections.Mapping):
            try:
                self._rdescent_item(obj, path)
            except:
                pass
            obj = obj.values()

        if isinstance(obj, collections.Sequence):
            for item in obj:
                try:
                    self._rdescent_item(obj, path)
                except:
                    pass
                self.recursive_descent(item, path)

    def _rdescent_item(self, obj, path):
        item = self(obj, path)
        if not id(item) in self.visited:
            self.visited.append(id(item))
            if isinstance(item, queryset):
                self.queryset.extend(item)
            else:
                self.queryset.append(item)

    def expand_asterisk(self, obj, path):
        if isinstance(obj, collections.Mapping):
            seq = obj.items()
        else:
            seq = iter(obj)

        expansions = (self(o, path) for o in obj)
        result = queryset()
        for item in obj:
            try:
                item = self(item, path)
            except (KeyError, IndexError):
                continue
            if isinstance(item, queryset):
                result.extend(item)
            else:
                result.append(item)
        return result

    def __call__(self, obj, path):
        path = path.strip()
        if not path:
            return obj
        elif path.startswith('.*'):
            path = path[2:]
            return self.expand_asterisk(obj, path)
        elif path.startswith('..'):
            path = path[1:]
            self.visited = set()
            self.queryset = []
            self.recursive_descent(obj, path)
            return self.queryset
        else:
            # simple walk to object
            obj, path = self.walk(obj, path)
            if path:
                return self(obj, path)
            else:
                return obj


if __name__ == '__main__':
    from chips.debug import *

    qtest = { 'meta': { 'title': 'Foo', 'db_name': 'foobar' },
              'content': [ { 'type': 'text',
                             'text': 'some text' },
                           { 'type': 'multiple-choice',
                             'items': [ { 'text': 'Foo' },
                                        { 'text': 'Bar' } ] }]}
    print(array_op('#(@.foo)'))

#    print(getitem(qtest, '$.content.*.items'))
#    print('$.foo.bar[#(@.name)]')
#    print(list(iterpath('$.foo.bar[#(@.name)]')))
#    print(array_op('[#(@.name)]'))
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
